﻿using System;
using System.Windows;
using System.Windows.Media;
using System.Windows.Shapes;

namespace Southridge.Controls
{
    public class CalloutShape : Shape
    {
        #region ArrowWidth

        /// <summary>
        /// ArrowWidth Dependency Property
        /// </summary>
        public static readonly DependencyProperty ArrowWidthProperty =
            DependencyProperty.Register("ArrowWidth", typeof(double), typeof(CalloutShape),
                new FrameworkPropertyMetadata((double)18.0,
                    FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// Gets or sets the ArrowWidth property.  This dependency property 
        /// indicates the width of the arrow on the callout.
        /// </summary>
        public double ArrowWidth
        {
            get { return (double)GetValue(ArrowWidthProperty); }
            set { SetValue(ArrowWidthProperty, value); }
        }

        #endregion

        #region ArrowHeight

        /// <summary>
        /// ArrowHeight Dependency Property
        /// </summary>
        public static readonly DependencyProperty ArrowHeightProperty =
            DependencyProperty.Register("ArrowHeight", typeof(double), typeof(CalloutShape),
                new FrameworkPropertyMetadata((double)12.0,
                    FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// Gets or sets the ArrowHeight property.  This dependency property 
        /// indicates the Height of the arrow on the callout.
        /// </summary>
        public double ArrowHeight
        {
            get { return (double)GetValue(ArrowHeightProperty); }
            set { SetValue(ArrowHeightProperty, value); }
        }

        #endregion

        #region ArrowOffset

        /// <summary>
        /// ArrowOffset Dependency Property
        /// </summary>
        public static readonly DependencyProperty ArrowOffsetProperty =
            DependencyProperty.Register("ArrowOffset", typeof(double), typeof(CalloutShape),
                new FrameworkPropertyMetadata((double)0,
                    FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// Gets or sets the ArrowOffset property.  This dependency property 
        /// indicates the offset of the arrow relative to its placement.
        /// </summary>
        public double ArrowOffset
        {
            get { return (double)GetValue(ArrowOffsetProperty); }
            set { SetValue(ArrowOffsetProperty, value); }
        }

        #endregion

        #region ArrowPlacement

        /// <summary>
        /// ArrowPlacement Dependency Property
        /// </summary>
        public static readonly DependencyProperty ArrowPlacementProperty =
            DependencyProperty.Register("ArrowPlacement", typeof(ArrowPlacement), typeof(CalloutShape),
                new FrameworkPropertyMetadata((ArrowPlacement)ArrowPlacement.Top,
                    FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// Gets or sets the ArrowPlacement property.  This dependency property 
        /// indicates the offset of the arrow relative to its placement.
        /// </summary>
        public ArrowPlacement ArrowPlacement
        {
            get { return (ArrowPlacement)GetValue(ArrowPlacementProperty); }
            set { SetValue(ArrowPlacementProperty, value); }
        }

        #endregion

        #region ArrowAlignment

        /// <summary>
        /// ArrowAlignment Dependency Property
        /// </summary>
        public static readonly DependencyProperty ArrowAlignmentProperty =
            DependencyProperty.Register("ArrowAlignment", typeof(ArrowAlignment), typeof(CalloutShape),
                new FrameworkPropertyMetadata((ArrowAlignment)ArrowAlignment.TopLeft,
                    FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// Gets or sets the ArrowAlignment property.  This dependency property 
        /// indicates the offset of the arrow relative to its placement.
        /// </summary>
        public ArrowAlignment ArrowAlignment
        {
            get { return (ArrowAlignment)GetValue(ArrowAlignmentProperty); }
            set { SetValue(ArrowAlignmentProperty, value); }
        }

        #endregion

        #region RadiusX

        /// <summary>
        /// RadiusX Dependency Property
        /// </summary>
        public static readonly DependencyProperty RadiusXProperty =
            DependencyProperty.Register("RadiusX", typeof(double), typeof(CalloutShape),
                new FrameworkPropertyMetadata((double)10.0,
                    FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// Gets or sets the RadiusX property.  This dependency property 
        /// indicates the offset of the arrow relative to its placement.
        /// </summary>
        public double RadiusX
        {
            get { return (double)GetValue(RadiusXProperty); }
            set { SetValue(RadiusXProperty, value); }
        }

        #endregion

        #region RadiusY

        /// <summary>
        /// RadiusY Dependency Property
        /// </summary>
        public static readonly DependencyProperty RadiusYProperty =
            DependencyProperty.Register("RadiusY", typeof(double), typeof(CalloutShape),
                new FrameworkPropertyMetadata((double)10.0,
                    FrameworkPropertyMetadataOptions.AffectsRender));

        /// <summary>
        /// Gets or sets the RadiusY property.  This dependency property 
        /// indicates the offset of the arrow relative to its placement.
        /// </summary>
        public double RadiusY
        {
            get { return (double)GetValue(RadiusYProperty); }
            set { SetValue(RadiusYProperty, value); }
        }

        #endregion

        protected override Size MeasureOverride(Size constraint)
        {

            return GetNaturalSize();
        }

        private Size GetNaturalSize()
        {
            double strokeThickness = GetStrokeThickness();
            return new Size(strokeThickness, strokeThickness);
        }

        private double GetStrokeThickness()
        {
            double strokeThickness = StrokeThickness;
            bool isPenNoOp = true;
            if ((Stroke != null) && !double.IsNaN(strokeThickness))
            {
                isPenNoOp = (strokeThickness == 0);
            }
            return isPenNoOp ? 0.0d : Math.Abs(strokeThickness);
        }

        Rect _rect;

        protected override Size ArrangeOverride(Size finalSize)
        {
            double penThickness = GetStrokeThickness();
            double margin = penThickness / 2;

            _rect = new Rect(margin, margin,
                 Math.Max(0, finalSize.Width - penThickness),
                 Math.Max(0, finalSize.Height - penThickness));

            return base.ArrangeOverride(finalSize);
        }


        protected override System.Windows.Media.Geometry DefiningGeometry
        {
            get
            {
                // calculate the geometry for the rectangle

                double top = _rect.Top;
                double left = _rect.Left;
                double width = _rect.Width;
                double height = _rect.Height;

                switch (ArrowPlacement)
                {
                    case ArrowPlacement.Top: top += ArrowHeight; height -= ArrowHeight; break;
                    case ArrowPlacement.Bottom: height -= ArrowHeight; break;
                    case ArrowPlacement.Left: left += ArrowWidth; width -= ArrowWidth; break;
                    case ArrowPlacement.Right: width -= ArrowWidth; break;
                }

                RectangleGeometry rectangle = new RectangleGeometry(new Rect(left, top, width, height), RadiusX, RadiusY);

                // if the width or height of the arrow is 0 then we can return the
                // rectangle we just created

                if (ArrowWidth == 0 || ArrowHeight == 0) return rectangle;
                
                // otherwise, we calculate the geometry for the arrow

                Point p1, p2, p3;
                switch (ArrowPlacement)
                {
                    case ArrowPlacement.Top:
                        p1 = new Point(0, ArrowHeight + StrokeThickness);
                        p2 = new Point(ArrowWidth / 2, 0);
                        p3 = new Point(ArrowWidth, ArrowHeight + StrokeThickness);
                        break;

                    case ArrowPlacement.Bottom:
                        p1 = new Point(0, height - StrokeThickness);
                        p2 = new Point(ArrowWidth / 2, height+ArrowHeight);
                        p3 = new Point(ArrowWidth, height - StrokeThickness);
                        break;

                    case ArrowPlacement.Left:
                        p1 = new Point(ArrowWidth+StrokeThickness, 0);
                        p2 = new Point(0, ArrowHeight/2);
                        p3 = new Point(ArrowWidth + StrokeThickness, ArrowHeight);
                        break;

                    case ArrowPlacement.Right:
                        p1 = new Point(width - StrokeThickness, 0);
                        p2 = new Point(width + ArrowWidth, ArrowHeight / 2);
                        p3 = new Point(width-StrokeThickness, ArrowHeight);
                        break;

                    default:
                        p1 = new Point();
                        p2 = new Point();
                        p3 = new Point();
                        break;
                }

                // calculate the offsets according to the value of ArrowPlacement
                // and the values of RadiusX and RadiusY

                double xOffset = 0;
                double yOffset = 0;
                bool isTopOrBottom = (ArrowPlacement == ArrowPlacement.Top || ArrowPlacement == ArrowPlacement.Bottom);

                switch (ArrowAlignment)
                {
                    case ArrowAlignment.TopLeft:
                        if (isTopOrBottom) xOffset += (RadiusX + ArrowOffset); else yOffset += (RadiusX + ArrowOffset);
                        break;

                    case ArrowAlignment.BottomRight:
                        if (isTopOrBottom) xOffset += (width - (ArrowWidth + RadiusX + ArrowOffset)); else yOffset += (height - (ArrowHeight + RadiusY + ArrowOffset));
                        break;

                    case ArrowAlignment.Center:
                        if (isTopOrBottom) xOffset += ((width/2 - ArrowWidth/2)) + ArrowOffset; else yOffset += ((height/2 - ArrowHeight/2) + ArrowOffset);
                        break;
                }

                // update the point values with the offsets

                p1.X += xOffset; p1.Y += yOffset;
                p2.X += xOffset; p2.Y += yOffset;
                p3.X += xOffset; p3.Y += yOffset;

                // now create the arrow geometry with the points we've already calculated

                PathFigure p = new PathFigure();
                p.StartPoint = p1;

                p.Segments = new PathSegmentCollection();
                p.Segments.Add(new LineSegment(p1, false));
                p.Segments.Add(new LineSegment(p2, true));
                p.Segments.Add(new LineSegment(p3, true));
                p.Segments.Add(new LineSegment(p1, false));

                PathGeometry arrow = new PathGeometry();
                arrow.Figures.Add(p);

                // combine the geometries and return

                return new CombinedGeometry(GeometryCombineMode.Union, arrow, rectangle);
            }
        }
    }

    public enum ArrowPlacement
    {
        Top,
        Bottom,
        Left,
        Right
    }

    public enum ArrowAlignment
    {
        TopLeft,
        Center,
        BottomRight
    }
}
